﻿// -----------------------------------------------------------------------
// <copyright file="FilterContract.cs">
// Copyright (c) 2013 Andrey Veselov. All rights reserved.
// License:  Microsoft Public License (MS-PL)
// Contacts: http://andrey.moveax.com  andrey@moveax.com
// </copyright>
// -----------------------------------------------------------------------
// More details at http://www.w3.org/TR/PNG/#9Filters

namespace PortableImagingLibrary.Png.Filters
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    internal class AdaptiveFilter : IFilter
    {
        private enum LineFilteringType : byte
        {
            None = 0,
            Sub = 1,
            Up = 2,
            Average = 3,
            Paeth = 4
        }

        private class FilterInfo
        {
            public LineFilteringType Type { get; set; }
            public Func<int, byte[], byte, byte> Func { get; set; }
        }

        private readonly IEnumerable<FilterInfo> _filters;
        private byte[] _previousLine = null;
        private byte[] _temporaryLine = null;

        public AdaptiveFilter()
        {
            this._filters = new List<FilterInfo>() {
                new FilterInfo() { Type = LineFilteringType.None, Func = this.None },
                new FilterInfo() { Type = LineFilteringType.Sub, Func = this.Sub },
                new FilterInfo() { Type = LineFilteringType.Up, Func = this.Up },
                new FilterInfo() { Type = LineFilteringType.Average, Func = this.Average },
                new FilterInfo() { Type = LineFilteringType.Paeth, Func = this.Paeth }
            };
        }

        #region IFilter Members

        public void ReconstructLine(byte[] sourceLine, byte bytesPerSample)
        {
            var filterType = (LineFilteringType)sourceLine[0];
            FilterInfo filter = this._filters.First(f => f.Type == filterType);

            if (filterType != LineFilteringType.None) {
                for (var pos = 1; pos < sourceLine.Length; pos++)
                    sourceLine[pos] = (byte)(sourceLine[pos] + filter.Func(pos, sourceLine, bytesPerSample));
            }

            if (this._previousLine == null)
                this._previousLine = new byte[sourceLine.Length];

            Array.Copy(sourceLine, this._previousLine, sourceLine.Length);
        }

        public void FilterLine(byte[] sourceLine, byte bytesPerSample)
        {
            // Select best filter
            FilterInfo selectedFilter = this._filters.First();
            uint minimalSum = uint.MaxValue;

            foreach (LineFilteringType filterType in Enum.GetValues(typeof(LineFilteringType))) {
                FilterInfo filter = this._filters.First(f => f.Type == filterType);

                uint sum = 0;
                for (var pos = 1; pos < sourceLine.Length; pos++)
                    sum += (byte)(sourceLine[pos] - filter.Func(pos, sourceLine, bytesPerSample));

                if (sum < minimalSum) {
                    minimalSum = sum;
                    selectedFilter = filter;
                }
            }

            // Save original data to temporary buffer
            if (this._temporaryLine == null)
                this._temporaryLine = new byte[sourceLine.Length];

            Array.Copy(sourceLine, this._temporaryLine, sourceLine.Length);

            // Filter line
            sourceLine[0] = (byte)selectedFilter.Type;
            for (var pos = 1; pos < sourceLine.Length; pos++)
                sourceLine[pos] = (byte)(sourceLine[pos] - selectedFilter.Func(pos, this._temporaryLine, bytesPerSample));

            // Save ogirinal data for next line filtering
            if (this._previousLine == null)
                this._previousLine = new byte[sourceLine.Length];

            Array.Copy(this._temporaryLine, this._previousLine, this._temporaryLine.Length);
        }

        #endregion IFilter Members

        #region Filter methods

        private byte None(int pos, byte[] line, byte bytesPerSample)
        {
            return 0;
        }

        private byte Sub(int pos, byte[] line, byte bytesPerSample)
        {
            return pos > bytesPerSample ?
                line[pos - bytesPerSample] :
                (byte)0;
        }

        private byte Up(int pos, byte[] line, byte bytesPerSample)
        {
            return this._previousLine != null ?
                this._previousLine[pos] :
                (byte)0;
        }

        private byte Average(int pos, byte[] line, byte bytesPerSample)
        {
            byte a = pos > bytesPerSample ?
                line[pos - bytesPerSample] :
                (byte)0;

            byte b = this._previousLine != null ?
                this._previousLine[pos] :
                (byte)0;

            return (byte)((a + b) >> 1);
        }

        private byte Paeth(int pos, byte[] line, byte bytesPerSample)
        {
            byte a = pos > bytesPerSample ?
                line[pos - bytesPerSample] :
                (byte)0;

            byte b = this._previousLine != null ?
                this._previousLine[pos] :
                (byte)0;

            byte c = pos > bytesPerSample && this._previousLine != null ?
                this._previousLine[pos - bytesPerSample] :
                (byte)0;

            // estimate paeth predictor
            var p = a + b - c;
            var pa = Math.Abs(p - a);
            var pb = Math.Abs(p - b);
            var pc = Math.Abs(p - c);

            if ((pa <= pb) && (pa <= pc))
                return a;

            return pb <= pc ? b : c;
        }

        #endregion
    }
}
